let helper = require('../helper/index');
let browserHelper = require('../puppeteer/index');
let wkHtmlToPdfHelper = require('../wkhtmltopdf/index');
const Lodash = require('lodash');
const pdfMeta = require('../pdfmeta');
const fs =require('fs');
const path =require('path');
const urlencode = require('urlencode');
const pdfTool = require('../pdftool');
const Notify = require('../helper/notify');
const joi = require('joi');
const axios = require("axios");
const Storage = require("../storage/index");
const Config = require("../../config")
const fetch = require('node-fetch');

// 定时删除PDF文件任务
require('../cron/pdfclean');

const getAllSpiders = function (){
    let extendSpiders = {};
    const extendSpidersFile = __dirname + '/../../public/spider/index.js';
    if(fs.existsSync(extendSpidersFile)){
        extendSpiders = require(extendSpidersFile);
    }
    const allSpiders =  Object.assign(require("../spider"),extendSpiders);
    
    helper.log("Spider loaded:" + Lodash.keys(allSpiders).join('|'))
    
    return allSpiders;
}

const AllSpiders = getAllSpiders();

/** TODO 参数验证 **/
// let optionSchemaData = joi.object({
//     'pageUrl': joi.string().optional().uri({scheme: ['http','https','data'],allowRelative:false,allowQuerySquareBrackets:true}).description("目标网页的URI").error(new Error('invalid param pageUrl')),
//     'html': joi.string().optional().description("页面内容HTML,与pageUrl穿其一").error(new Error('invalid param html')),
//    
//     'timeout': joi.number().integer().min(2000).max(60000).default(30000).optional().description("超时时间").error(new Error('invalid param timeout')),
//     'ignoreMeta': joi.boolean().default(false).optional().description("是否忽略，PDF Meta信息").error(new Error('invalid param ignoreMeta')),
//     'delay': joi.number().integer().required().default(0).description("在页面完成后，延迟毫秒数").error(new Error('invalid param delay')),
//     'width': joi.number().integer().required().min(0).default(0).description("在页面完成后，延迟毫秒数").error(new Error('invalid param width')),
//     'height': joi.number().integer().required().min(0).default(0).description("在页面完成后，延迟毫秒数").error(new Error('invalid param height')),
//     'checkPageCompleteJs': joi.string().required().default('"true"').description(" 检查页面是否渲染完成的js表达式").error(new Error('invalid param checkPageCompleteJs')),
//    
//     'metaInfo':null,
//    
//    
//     "element" : joi.string().required().description("要截取的节点选择器,可选，默认body"),
//     "elements" : joi.array().single().items(joi.string().required()).min(1).max(100).description("要截取的节点选择器列表"),
//     "bookConfig" : joi.object({}).options({allowUnknown:true}).description("bookjs-eazy配置"),
//     "bookStyle" : joi.string().default('').description("模版使用到的的css样式"),
//     "bookTpl" : joi.string().default('').description("bookjs-eazy模板内容"),
//    
//    
//     "orientation" : joi.string().default('portrait').description("纸张方向：portrait/landscape"),
//     "pageSize" : joi.string().default('').description("纸张方向：\"portrait\"，\"landscape\""),
//
//     'pageWidth': joi.number().integer().required().min(0).default(0).description("纸张宽度（毫米）"),
//     'pageHeight': joi.number().integer().required().min(0).default(0).description("纸张高度（毫米）"),
//
//
//
// }).or('pageUrl','html');
//
//
// let allowOptionNames = Lodash.keys(optionSchemaData);
// let optionSchema = joi.compile(optionSchemaData);
// let res = optionSchema.validate(Lodash.pick(option,allowOptionNames),{ abortEarly: false,convert: true });
//
// if(res.error){
//     throw res.error;
// }
     

const processPdfMeta = async function (pdfPathInfo,bookJsMetaInfo,ignoreMeta){
    helper.log("process pdf meta:" + pdfPathInfo.relatePath);
    await pdfMeta.setPdfMetaInfo(pdfPathInfo.fullPath, bookJsMetaInfo,ignoreMeta);
    helper.log("process pdf meta complete:" + pdfPathInfo.relatePath);
};

const renderPdf = function (req, res, next) {
    let notify = new Notify(req,res);
    req.body.timeout = ~~req.body.timeout || 60000;
    let ignoreMeta = !! req.body.ignoreMeta;
    
    let postParam = req.body;
    browserHelper.makePdf(postParam)
        .then(async function (info) {
            let pdfPathInfo = info.pathInfo;
            await helper.assertFileReadable(pdfPathInfo.fullPath,"make pdf file failed");

            let metaInfo = info.metaInfo;
            await processPdfMeta(pdfPathInfo,metaInfo,ignoreMeta);
            await Storage.uploadFile(pdfPathInfo.relatePath,pdfPathInfo.fullPath)
    
            notify.send(helper.successMsg({
                file: pdfPathInfo.relatePath
            }));
        }).catch(e => {
            let errorMsg = e.toString();
            if (/ERR_CONNECTION_REFUSED/.test(errorMsg)) {
                errorMsg = "PDF生成服务器无法访问到页面的URL"
            }
            helper.error("renderPdf:" + e);
            notify.send(helper.failMsg("fail:" + errorMsg));
        });
};

const renderBook = function (req, res, next) {
    req.body.checkPageCompleteJs = "window.status === 'PDFComplete'";
    req.body.timeout = req.body.timeout || 60000;
    return renderPdf(req, res, next);
};

/**
 * 从网页中截取一张图片
 *
 * @param req
 * @param res
 * @param next
 */
const renderImage = function (req, res, next) {
    let notify = new Notify(req,res);
    let postParam = req.body;
    let element = (postParam.element || 'body') + '';
    browserHelper.loadPage({
        pageUrl: postParam.pageUrl,
        html: postParam.html,
        timeout: ~~postParam.timeout,
        delay: ~~postParam.delay,
        width: ~~postParam.width,
        height: ~~postParam.height,
        checkPageCompleteJs: postParam.checkPageCompleteJs,
    }, async function (page) {
        let res = await browserHelper.screenshotDOMElement(page, element,'png',false,'base64');
        if (res.base64) {
            notify.send(helper.successMsg({image: res.base64}))
        } else {
            notify.send(helper.failMsg("render fail"));
        }
    }).catch(function (e) {
        helper.error("renderImage:" + e);
        notify.send(helper.failMsg("fail:" + e.toString()));
    });
};

/**
 * 从网页中截取多张图片
 * 
 * @param req
 * @param res
 * @param next
 */
const renderImages = function (req, res, next) {
    let postParam = req.body;
    let elements = postParam.elements || [];

    let notify = new Notify(req,res);
    browserHelper.loadPage({
        pageUrl: postParam.pageUrl,
        html: postParam.html,
        timeout: ~~postParam.timeout,
        delay: ~~postParam.delay,
        width: ~~postParam.width,                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      
        height: ~~postParam.height,
        checkPageCompleteJs: postParam.checkPageCompleteJs,
    }, async function (page) {
        let images = await browserHelper.screenshotDOMElements(page, elements,'png',false,'base64');
        if (images) {
            let ret = {};
            for (const selector in images) {
                ret[selector] = images[selector].map(v => v.base64);
            }
            notify.send(helper.successMsg({images: ret}))
        } else {
            notify.send(helper.failMsg("render fail"));
        }
    }).catch(function (e) {
        helper.error("renderImages:" + e);
        notify.send(helper.failMsg("fail:" + e.toString()));
    });
};

/**
 * 根据PDF路径，下载PDF
 * @param req
 * @param res
 * @param next
 */
const downloadPdf = function (req, res, next) {
    let fileName = req.query.fileName || 'output.pdf';
    let file = req.params[0].replace(/\.\./g, "");
    if (/[^\w \-\/\.]/.test(file)) {
        res.sendStatus(400);
        return;
    }
    let fullPath = helper.getPublicPath(file);
    fs.access(fullPath, fs.constants.R_OK,function (err) {
        if (err) {
            res.sendStatus(404);
            return;
        }

        fileName = fileName.replace(/[\r\n<>\\\/\|\:\'\"\*\?]/g, "");
        let headers = {
            "Content-type": "application/octet-stream",
            "Content-Transfer-Encoding": "binary",
        };
        let userAgent = req.headers['user-agent'];
        if ((/Safari/i).test(userAgent) && !(/Chrome/i).test(userAgent)) {
            headers['Content-Disposition'] = 'attachment; filename*=UTF-8\'\'' + urlencode(fileName, 'UTF-8');
        } else {
            headers['Content-Disposition'] = 'attachment;filename="' + urlencode(fileName, 'UTF-8') + '"';
        }

        try {
            res.sendFile(file, {
                headers: headers,
                root: helper.getPublicPath(),
            })
        } catch (e) {
            res.sendStatus(500);
        }
    });
};

/**
 * 生成bookjs-eazy页面模板
 * 
 * @param req
 * @param res
 * @param next
 * @returns {string}
 */
const makeBookTplHtml = function (req, res, next) {
    if (!Lodash.isObject(req.body.bookConfig)) {
        req.body.bookConfig = {};
    }

    let bookStyleJson = JSON.stringify(req.body.bookStyle || "");

    let bookTpl = req.body.bookTpl || '<div>内容为空</div>';
    let contentBox = '<div>' + bookTpl + '</div>';
    let baseUrl = 'http://127.0.0.1:' + (Config.serverPort || '3000') + '/';
    let bookConfig = Lodash.extend({
        pageSize: 'ISO_A4',
        orientation: 'portrait',// landscape
        padding: "20mm 10mm 20mm 10mm",
        toolBar: false
    }, req.body.bookConfig || {});
    bookConfig.start = true;
    bookConfig.contentBox = contentBox;

    let bookConfigStr = JSON.stringify(bookConfig);
    let htmlContent = `<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
    <meta charset="UTF-8">
    <title>screenshot-api-server</title>
    <base href="${baseUrl}" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge, chrome=1">
    <meta name="renderer" content="webkit">
    <meta name="format-detection" content="telephone=no">
    <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=0">
    <script src="/static/js/polyfill.min.js"></script>
    <script src="/static/js/jquery.min.js"></script>
    <script src="/static/js/lodash.min.js"></script>
    <script src="/static/js/bookjs/latest/bookjs-eazy.min.js"></script>
</head>
<body>
<style id="book-style" type="text/css" rel="stylesheet"></style>
<script>
    document.getElementById('book-style').appendChild(document.createTextNode(${bookStyleJson}));
</script>
<script>
    bookConfig = ${bookConfigStr};
</script>
</body>
</html>
`;

    return htmlContent;
};

const renderBookTplHtml = function (req, res, next) {
    req.send(makeBookTplHtml(req, res, next));
};

const renderBookTpl = function (req, res, next) {
    req.body.html = makeBookTplHtml(req, res, next);
    renderBook(req, res, next);
};

const renderBookPage = function (req, res, next) {
    let tpl_id = req.query.tpl_id;
    let bookConfig = helper.cache.get(tpl_id) || {start: true, contentBox: '<h2>链接已失效</h2>'};
    res.render('tpl-html.art', {
        bookConfig: bookConfig,
    });
};

const normalizeMetaInfo = async function (req,page) {
    let bookJsMetaInfo = {
        information: {}
    };
    
    if(page){
        let ret = await page.evaluate("window.bookJsMetaInfo");
        if(ret !== null && ret !== undefined){
            bookJsMetaInfo = ret;
        }
    }
    

    if(Lodash.isPlainObject(req.body.metaInfo)){
        let metaInfo = req.body.metaInfo;
        let information = metaInfo.information || {};
        
        /** old version bookjs-eazy meta options **/
        if (typeof metaInfo.Author === 'string') {
            if(metaInfo.Author){
                information.author = metaInfo.Author;
            }

            delete metaInfo.Author;
        }
        
        if (typeof metaInfo.Subject === 'string') {
            if(metaInfo.Subject){
                information.subject = metaInfo.Subject;
            }

            delete metaInfo.Subject;
        }
        if (typeof metaInfo.Keywords === 'string') {
            if(metaInfo.Keywords){
                information.keywords = metaInfo.Keywords;
            }
            
            delete metaInfo.Keywords;
        }
        delete metaInfo.title;
        metaInfo.information = information;
        
        bookJsMetaInfo = metaInfo;
    }
    
    if(Lodash.isEmpty(bookJsMetaInfo.information.subject)){
        delete bookJsMetaInfo.information.subject;
    }
    if(Lodash.isEmpty(bookJsMetaInfo.information.keywords)){
        delete bookJsMetaInfo.information.keywords;
    }
    if(Lodash.isEmpty(bookJsMetaInfo.information.author)){
        delete bookJsMetaInfo.information.author;
    }

    return bookJsMetaInfo
};

/**
 * 使用wkhtmltopdf URL转PDF
 * 
 * @param req
 * @param res
 * @param next
 * 
 * POST JSON
 * {
 *   "pageUrl":"http://localhost:8080/eazy-test.html",
 *   "orientation":"landscape",
 *   "pageSize":"A4" 
 *   // pageWidth : 192, pageHeight:97
 *   // windowStatus : "PDFComplete"
 * }
 */
const renderWkHtmlToPdf = function (req, res, next) {
    let postParam = req.body;
    let ignoreMeta = !!req.body.ignoreMeta;
    if(!postParam.pageSize){
        postParam.pageSize = {
            pageWidth : ~~postParam.pageWidth,
            pageHeight : ~~postParam.pageHeight,
        }
    }
    
    let notify = new Notify(req,res);
    normalizeMetaInfo(req)
        .then(async function (bookJsMetaInfo) {
            let pdfPathInfo = helper.makePdfFileInfo();
            await wkHtmlToPdfHelper.wkHtmlToPdf(postParam.pageUrl, pdfPathInfo.fullPath, postParam.pageSize, postParam.orientation, postParam.delay, postParam.timeout,postParam.windowStatus);
            await helper.assertFileReadable(pdfPathInfo.fullPath,"make pdf file failed");
            await processPdfMeta(pdfPathInfo,bookJsMetaInfo,ignoreMeta);
            await Storage.uploadFile(pdfPathInfo.relatePath,pdfPathInfo.fullPath)
            
            notify.send(helper.successMsg({
                file: pdfPathInfo.relatePath,
            }));
        })
        .catch(function (e) {
            helper.error("renderWkHtmlToPdf:" + e);
            notify.send(helper.failMsg("fail:" + e.toString()));
        });
};

/**
 * 转换bookjs-eazy编写的页面转PDF
 * 
 * @param req
 * @param res
 * @param next
 */
const renderWkHtmlToPdfBook = function (req, res, next) {
    req.body.windowStatus = 'PDFComplete';
    renderWkHtmlToPdf(req,res,next);
};

const renderPdfProcess = async function(req, res, next){
    let options =  req.body.options || {};
    let timeout =  req.body.timeout;


    if(!(options.pdf instanceof Array)){
        res.send(helper.failMsg("options.pdf is required , must a array(string)"));
        return;
    }

    let pdfFiles = [];
    for (let i =0;i<options.pdf.length;i++){
        let relatePath = options.pdf[i];
        if(typeof relatePath !== 'string'){
            res.send(helper.failMsg("options.pdf["+i+"] is must a string, relate path"));
            return;
        }
        if(relatePath.includes('..') || /[^\w.\-\/]/.test(relatePath)){
            res.send(helper.failMsg("options.pdf["+i+"] is unsafe relate path"));
            return;
        }

        pdfFiles.push(helper.getPublicPath(relatePath));
    }

    options.pdf = pdfFiles;
    let pdfPathInfo = helper.makePdfFileInfo();
    options.output = pdfPathInfo.fullPath;

    let notify = new Notify(req,res);
    await pdfTool.process(options,timeout)
        .then(function(){
            notify.send(helper.successMsg({file: pdfPathInfo.relatePath}));
        })
        .catch(function (e) {
            notify.send(helper.failMsg("fail:" + e.toString()));
        });
};

const proxyAssert = async function(req, res, next){
    let url = req.query.url;
    let timeout = ~~(req.query.timeout);
    if(timeout <= 0){
        timeout = 30000;
    }else if(timeout < 3000){
        timeout = 3000;
    }else if(timeout > 120000){
        timeout = 120000;
    }
    if(!url){
        res.status(400).send("EMPTY URL");
        return;
    }
    let uri = null;
    try{
         uri =  new URL(url);
    }catch (e) {
        res.status(404).send("INVALID URL");
        return;
    }
    
    if(Config.blockIpUrl){
        if(/^\d+\.\d+\.\d+\.\d+$/.test(uri.hostname)){
            res.status(401).send("BLOCK_IP_URL");
            return;
        }
    }

    axios.get(url,{responseType: 'arraybuffer',timeout: timeout})
        .then(function (response) {
            let contentType = response.headers["content-type"];
            if (contentType){
                res.header("content-type",contentType)
                if (contentType.includes('image')){
                    res.header('Cache-Control', 'max-age=' + 3600)
                }
            }
            
            res.status(response.status).send(response.data);
        })
        .catch(function (err) {
            res.status(500).send('' + err);
        })
}

const renderSpider = async function (req,res){

    let postParam = req.body;
    let spiderName = postParam.spiderName
    let notify = new Notify(req,res);

    try{
        let spiderFunc = AllSpiders[spiderName];
        if(spiderFunc === undefined){
            throw "can not found spider, Name:" + spiderName;
        }

        let apiData = null;
        await browserHelper.getPage(postParam,async function (page){
            await browserHelper.hideWebDriverSpecific(page);
            apiData = await spiderFunc(page,postParam)
        });

        notify.send(helper.successMsg(apiData));
    }catch (e){
        notify.send(helper.failMsg(e.toString()));
    }
}


let jqueryContent = null;
let promptContent = null;
const getInjectScript = async function(preload,monkey){
    if(jqueryContent === null){
        jqueryContent = await fs.readFileSync(path.join(__dirname,'../../static/js/jquery.min.js'),'utf-8' );
    }

    if(promptContent === null){
        promptContent = await fs.readFileSync(path.join(__dirname,'../../static/js/prompt.js'),'utf-8' );
    }
    
    return `
(function(){
    let Monkey = {
                data: __monkeyData,
                end: __monkeyEnd,
                screenshot: __monkeyScreenshot,
                screenshots: __monkeyScreenshots,
                getCookies: __monkeyGetCookies,
                error: __monkeyError,
                requireScriptUrls: __monkeyRequireScriptUrls,
                fetchJson: __monkeyFetchJson,
    };
    ${promptContent}
    Monkey.onDialog = window.__monkeyOnDialog;

    ${preload}
    window.addEventListener('load',function(){
         if(!window.__jQuery){
            ${jqueryContent}
            window.__jQuery = jQuery; jQuery.noConflict(true);
            
            (async function($,jQuery,Monkey){ 
                ${monkey} 
            })(window.__jQuery,window.__jQuery,Monkey)
         }
    });
})();
`;
}

const renderMonkey = async function (req,res){
    let postParam = req.body;
    let pageUrl = postParam.pageUrl
    let preload = postParam.preload || '';
    let monkey = postParam.monkey;
    postParam.timeout = 300000;
    let notify = new Notify(req,res);
    
    try{
        let apiData = {};
        //helper.cache.set()
        await browserHelper.getPage(postParam,async function (page){

            await new Promise(async function(resolve, reject){
                await browserHelper.hideWebDriverSpecific(page);
                helper.log('==== start inject monkey ====');
                await page.exposeFunction('__monkeyData', (key,data) => {
                    apiData[key] = data;
                    helper.log("monkeyData: " + key + ":",data)
                });
                await page.exposeFunction('__monkeyEnd', () => {
                    helper.log('==== monkeyEnd ====');
                    resolve();
                });
                await page.exposeFunction('__monkeyScreenshot', async function (key,selector,type) {
                    helper.log("__monkeyScreenshot: " + key + "," + selector + "," + type)
                    let val = await browserHelper.screenshotDOMElement(page,selector,type);
                    if(key === null){
                        return val;
                    }else{
                        apiData[key] = val;
                    }
                });
                await page.exposeFunction('__monkeyScreenshots', async function (key,selector,type) {
                    helper.log("__monkeyScreenshots: " + key + "," + selector + "," + type);
                    let val = await browserHelper.screenshotDOMElements(page,selector,type);
                    if(key === null){
                        return val;
                    }else{
                        apiData[key] = val;
                    }
                })

                await page.exposeFunction('__monkeyGetCookies', async function () {
                    helper.log("__monkeyGetCookies: ")

                    return await page.cookies();
                })

                await page.exposeFunction('__monkeyFetchJson', async function (url,options) {
                    helper.log("__monkeyFetchJson:",url,options);

                    options = options || {};
                    options.headers = options.headers || {};
                    options.headers['Accept'] =  'application/json';
                    let resp = await fetch(url, options);
                    return await resp.json();
                })
                
                await page.exposeFunction('__monkeyError', (error) => {
                    let errorMsg = "monkeyError: " + error;
                    helper.error(error)
                    reject(errorMsg);
                });

                await page.exposeFunction('__monkeyRequireScriptUrls', async function (urls){
                    helper.info("__monkeyRequireScriptUrls:",urls);
                    let list = [];
                    for (let i = 0; i < urls.length; i++) {
                        let url = urls[i];
                        list.push(page.addScriptTag({url:url}))
                    }
                    
                    await Promise.all(list);
                    helper.info("__monkeyRequireScriptUrls Complete")
                });
                
                await page.evaluateOnNewDocument(await getInjectScript(preload,monkey)).catch(function (e){
                    reject(e)
                })

                await page.goto(pageUrl).catch(function (e){
                    reject(e)
                })
            })
        })

        notify.send(helper.successMsg(apiData));
    }catch (e){
        notify.send(helper.failMsg(e.toString()));
    }
}

module.exports = {
    proxyAssert,
    renderPdf,
    renderImage,
    renderImages,
    renderBook,
    downloadPdf,
    renderBookTpl,
    renderBookPage,
    renderWkHtmlToPdfBook,
    renderWkHtmlToPdf,
    renderPdfProcess,
    renderSpider,
    renderMonkey,
};
